Workflow dashboard with mermaid.js

工作流概述

这是一个包含12个节点的复杂工作流,主要用于自动化处理各种任务。

工作流源代码

下载
{
  "id": "Um37boya1U0mnCjS",
  "meta": {
    "instanceId": "fb924c73af8f703905bc09c9ee8076f48c17b596ed05b18c0ff86915ef8a7c4a",
    "templateCredsSetupCompleted": true
  },
  "name": "Workflow dashboard with mermaid.js",
  "tags": [],
  "nodes": [
    {
      "id": "c1f74b3a-2ae6-4491-ac02-e1e0fd188664",
      "name": "When clicking ‘Test workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        1220,
        560
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "2aef0899-91bb-4141-9ec1-def1c31806ae",
      "name": "Respond with Mermaid",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2640,
        560
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/plain"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.mermaidChart }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "2c60a2e2-9f35-45dc-94d1-daf75314e934",
      "name": "List workflows",
      "type": "n8n-nodes-base.n8n",
      "position": [
        1620,
        360
      ],
      "parameters": {
        "filters": {},
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "id": "eW7IdTFt4ARJbEwR",
          "name": "Ted n8n account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ce4e49b9-e1ab-44d1-9490-5c685c9023d9",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1980,
        360
      ],
      "parameters": {
        "options": {},
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "wf_data"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bc48416a-01ff-45f4-9bf2-9f4a39054b54",
      "name": "Single workflow",
      "type": "n8n-nodes-base.n8n",
      "position": [
        1620,
        560
      ],
      "parameters": {
        "operation": "get",
        "workflowId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.query.wfid }}"
        },
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "id": "eW7IdTFt4ARJbEwR",
          "name": "Ted n8n account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "85f28981-544b-4510-b1ee-d4d538455074",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "position": [
        1420,
        460
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "load page",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "array",
                      "operation": "empty",
                      "singleValue": true
                    },
                    "leftValue": "={{ Object.keys($json?.query)}}",
                    "rightValue": "wfid"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "has wfid",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "a4c4c624-2ff5-4fc0-9bdb-802412a5d92f",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    },
                    "leftValue": "={{ Object.keys($json.query).join(',') }}",
                    "rightValue": "wfid"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "looseTypeValidation": true
        }
      },
      "typeVersion": 3
    },
    {
      "id": "95e0b67b-5e5b-4433-9822-da86900c12ca",
      "name": "Send Page",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2640,
        360
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "=<!DOCTYPE html>
<html lang=\"en\">
<head>
    <meta charset=\"UTF-8\">
    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
    <title>n8n Workflow Visualizer</title>
    <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\">
    <script src=\"https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js\"></script>
    <style>
      .card-img-container {
        height: 250px;
        overflow: hidden;
      }
      .card-img-container img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        object-position: top;
      }
    </style>
</head>
<body>
      <div class=\"container mt-4\">
          <h2>n8n automation flowcharts with mermaid.js</h2>
          <div id=\"workflows-container\"></div>
      </div>
      
<hr class=\"featurette-divider border-dark\" />

<section id=\"about\" class=\"container mt-3\">
  <h2 class=\"text-center mb-5\">About</h2>
  <div class=\"row\">

    <div class=\"col-lg-3 text-center\">
      <img src=\"https://gravatar.com/avatar/a551e67c6fe7affd5f882a527dee154bb6c3ac90cf878326accb3fb3ec77c8a6?r=pg&amp;d=retro&amp;size=200\" alt=\"Eduard\" class=\"rounded-circle mb-3\" width=\"140\" height=\"140\" />
      <h3 class=\"fw-normal\">Eduard</h3>
      <p><a class=\"btn btn-warning\" href=\"https://n8n.io/creators/eduard/\" target=\"_blank\">More templates</a></p>
      <p><a class=\"btn btn-outline-primary\" href=\"https://www.linkedin.com/in/parsadanyan/\" target=\"_blank\">LinkedIn</a></p>
    </div>

<div class=\"col-lg-9 text-center\">
  <div class=\"card shadow-sm mb-3\">
    <div class=\"card-img-container\">
      <img src=\"https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/Untitled_design_6_18de4ce8f4.png\" class=\"card-img-top\" alt=\"How to work with XML and SQL using n8n\" />
    </div>
    <div class=\"card-body\">
      <h5 class=\"card-title\">🦅 Workflow Dashboard for n8n</h5>
      <p class=\"card-text\">Get an overview of your n8n instance. This dashboard displays all workflows, nodes, and tags on a single page.</p>
      <a href=\"https://n8n.io/workflows/2269-get-a-birds-eye-view-of-your-n8n-instance-with-the-workflow-dashboard/\" class=\"btn btn-primary\" target=\"_blank\">Grab the template!</a>
    </div>
  </div>
</div>

  </div>
</section>

    <script>
        // JSON object containing workflow data with base webhook URL
        const workflowsData = {
            baseWorkflowUrl: \"{{ `${$json.instance_url}/workflow/`.replace(/([^:]\/)\/+/g, '$1') }}\", 
            baseWebhookUrl : \"{{ `${$json.instance_url}/${$json.webhook_path}/${$json.webhook_name}?wfid=`.replace(/([^:]\/)\/+/g, '$1') }}\", 
            workflows      : {{ JSON.stringify($json.wf_data) }}
        };

        document.addEventListener('DOMContentLoaded', () => {
            const workflowsContainer = document.getElementById('workflows-container');

            // Render initial page layout
            renderWorkflows(workflowsData.workflows);

            function renderWorkflows(workflows) {
                workflows.forEach(workflow => {
                    const card = createWorkflowCard(workflow);
                    workflowsContainer.appendChild(card);
                });
            }

			function createWorkflowCard(workflow) {
				const card = document.createElement('div');
				card.className = 'card mb-3';
				card.innerHTML = `
                <div class=\"card-body\">
                    <h5 class=\"card-title d-flex align-items-center\">
                        ${workflow.name}
                        <span class=\"badge bg-light-subtle border border-light-subtle text-light-emphasis rounded-pill ms-2\">
                            <a href=\"${workflowsData.baseWorkflowUrl}${workflow.id}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary-emphasis text-decoration-none\" title=\"Open workflow in a new window\"> 🔗 </a>
                        </span>
                    </h5>
                    <button class=\"btn btn-primary show-workflow-btn\" data-workflow-id=\"${workflow.id}\">Show Workflow</button>
                    <div class=\"mermaid-container mt-3\" style=\"display: none;\"></div>
                </div>
				`;

				const showWorkflowBtn = card.querySelector('.show-workflow-btn');
				const mermaidContainer = card.querySelector('.mermaid-container');
				let isLoaded = false;

				showWorkflowBtn.addEventListener('click', () => {
					if (!isLoaded) {
						fetchWorkflowDiagram(workflow.id, mermaidContainer);
						isLoaded = true;
						showWorkflowBtn.textContent = 'Hide Workflow';
					} else {
						if (mermaidContainer.style.display === 'none') {
							mermaidContainer.style.display = 'block';
							showWorkflowBtn.textContent = 'Hide Workflow';
						} else {
							mermaidContainer.style.display = 'none';
							showWorkflowBtn.textContent = 'Show Workflow';
						}
					}
				});

				return card;
			}

			function fetchWorkflowDiagram(workflowId, container) {
				const webhookUrl = `${workflowsData.baseWebhookUrl}${workflowId}`;
				fetch(webhookUrl)
					.then(response => response.text())
					.then(mermaidCode => {
						container.innerHTML = mermaidCode;
						container.style.display = 'block';
						mermaid.init(undefined, container);
					})
					.catch(error => {
						console.error('Error fetching workflow diagram:', error);
						container.innerHTML = '<p class=\"text-danger\">Error loading workflow diagram.</p>';
						container.style.display = 'block';
					});
			}

            // Initialize mermaid
            mermaid.initialize({ startOnLoad: false });
        });
    </script>

    <script>
        // Blog posts fetching and rendering
        document.addEventListener('DOMContentLoaded', () => {
            const blogPostsContainer = document.getElementById('blog-posts-container');
            const authors = ['Yulia Dmitrievna', 'Eduard Parsadanyan'];
            const maxPosts = 3;
    
            fetch('https://blog.n8n.io/rss/')
                .then(response => response.text())
                .then(str => new window.DOMParser().parseFromString(str, \"text/xml\"))
                .then(data => {
                    const items = data.querySelectorAll(\"item\");
                    let postCount = 0;
    
                    items.forEach(el => {
                        if (postCount >= maxPosts) return;
    
                        const author = el.querySelector(\"dc\\:creator\").textContent.trim();
                        if (authors.includes(author)) {
                            const title = el.querySelector(\"title\").textContent;
                            const link = el.querySelector(\"link\").textContent;
                            const imageUrl = el.querySelector(\"media\\:content\").getAttribute(\"url\");
    
                            const card = document.createElement('div');
                            card.className = 'col-md-4 mb-4';
                            card.innerHTML = `
                                <div class=\"card h-100\">
                                    <img src=\"${imageUrl}\" class=\"card-img-top\" alt=\"${title}\">
                                    <div class=\"card-body\">
                                        <h5 class=\"card-title\">${title}</h5>
                                        <p class=\"card-text\">By ${author}</p>
                                        <a href=\"${link}\" class=\"btn btn-primary\" target=\"_blank\">Read More</a>
                                    </div>
                                </div>
                            `;
    
                            blogPostsContainer.appendChild(card);
                            postCount++;
                        }
                    });
                })
                .catch(error => {
                    console.error('Error fetching blog posts:', error);
                    blogPostsContainer.innerHTML = '<p class=\"text-danger\">Error loading blog posts.</p>';
                });
        });
    </script>

    <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js\"></script>
</body>
</html>"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7f964438-a211-40bf-a991-a93848607513",
      "name": "Prepare workflow list",
      "type": "n8n-nodes-base.set",
      "position": [
        1800,
        360
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1ce915da-7ee4-487c-9233-0b603d4a913b",
              "name": "wf_data",
              "type": "object",
              "value": "={
\"id\"  :\"{{ $json.id }}\",
\"name\":\"{{ $json.name }}\"
}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d379a0b6-aaee-4f4d-91be-74d79c160bb8",
      "name": "CONFIG",
      "type": "n8n-nodes-base.set",
      "position": [
        2300,
        360
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "07da029f-3de3-45cb-8d33-798fa1a3d529",
              "name": "instance_url",
              "type": "string",
              "value": "={{$env[\"N8N_PROTOCOL\"]}}://{{$env[\"N8N_HOST\"]}}"
            },
            {
              "id": "f7dae7f3-e51b-4da3-ac8b-d198747679d2",
              "name": "webhook_name",
              "type": "string",
              "value": "={{ $('Webhook').params.path}}"
            },
            {
              "id": "185e41a7-8b61-46e3-99ea-0b0a66982080",
              "name": "webhook_path",
              "type": "string",
              "value": "={{$env[\"N8N_ENDPOINT_WEBHOOK\"] || \"webhook\"}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "bfc42a15-130c-4e81-9f89-c07b3bb56928",
      "name": "Code",
      "type": "n8n-nodes-base.code",
      "position": [
        1800,
        560
      ],
      "parameters": {
        "jsCode": "const workflow = $input.first().json;

// Extract nodes from the workflow
const nodes = workflow.nodes || [];

// Node types to exclude
const excludedNodeTypes = ['n8n-nodes-base.stickyNote'];

// Define shapes and their corresponding brackets
// https://mermaid.js.org/syntax/flowchart.html
const shapes = {
    'rect': ['[', ']'],
    'rhombus': ['{', '}'],
    'circle': ['((', '))'],
    'hexagon': ['{{', '}}'],
    'subroutine': ['[[', ']]'],
    'parallelogram': ['[\/', '\/]'],
    'wait': ['(', ')']
    // Add more shapes here as needed
};

// Define special shapes for specific node types
const specialShapes = {
    'n8n-nodes-base.if': 'rhombus',
    'n8n-nodes-base.switch': 'rhombus',
    'n8n-nodes-base.code': 'subroutine',
    'n8n-nodes-base.executeWorkflow': 'subroutine',
    'n8n-nodes-base.httpRequest':'parallelogram',
    'n8n-nodes-base.wait':'wait'
    // List more special node types
};

// Function to get the shape for a node type
function getNodeShape(nodeType) {
    return specialShapes[nodeType] || 'rect';
}

// Create a map of node names to their \"EL<N>\" identifiers, disabled status, and shape
const nodeMap = {};
let nodeCounter = 1;
nodes.forEach((node) => {
    if (!excludedNodeTypes.includes(node.type)) {
        const shape = getNodeShape(node.type);
        nodeMap[node.name] = {
            id: `EL${nodeCounter}`,
            disabled: node.disabled || false,
            shape: shape,
            brackets: shapes[shape] || shapes['rect'] // Default to rect if shape not found
        };
        nodeCounter++;
    }
});

// Function to convert special characters to HTML entities
function convertToHTMLEntities(str) {
    return str.replaceAll('\"',\"'\").replace(/[^\w\s-]/g, function(char) {
        return '&#' + char.charCodeAt(0) + ';';
    });
}

// Function to format node text (with strike-through if disabled)
function formatNodeText(nodeName, isDisabled) {
    const escapedName = convertToHTMLEntities(nodeName);
    return isDisabled ? `<s>${escapedName}</s>` : escapedName;
}

// Generate connections and isolated nodes
const connections = [];
const isolatedNodes = new Set(Object.keys(nodeMap));

if (workflow.connections) {
    Object.entries(workflow.connections).forEach(([sourceName, targetConnections]) => {
        Object.entries(targetConnections).forEach(([connectionType, targets]) => {
            targets.forEach(targetArray => {
                targetArray.forEach(target => {
                    const sourceNode = nodeMap[sourceName];
                    const targetNode = nodeMap[target.node];
                    if (sourceNode && targetNode) {
                        let connectionLine = `    ${sourceNode.id}${sourceNode.brackets[0]}${formatNodeText(sourceName, sourceNode.disabled)}${sourceNode.brackets[1]}`;
                        if (connectionType === 'main') {
                            connectionLine += ` -->`;
                        } else {
                            connectionLine += ` -.- |${connectionType}|`;
                        }
                        connectionLine += ` ${targetNode.id}${targetNode.brackets[0]}${formatNodeText(target.node, targetNode.disabled)}${targetNode.brackets[1]}`;
                        connections.push(connectionLine);
                        isolatedNodes.delete(sourceName);
                        isolatedNodes.delete(target.node);
                    }
                });
            });
        });
    });
}

// Add isolated nodes to the connections array
isolatedNodes.forEach(nodeName => {
    const node = nodeMap[nodeName];
    connections.push(`    ${node.id}${node.brackets[0]}${formatNodeText(nodeName, node.disabled)}${node.brackets[1]}`);
});

// Generate the Mermaid flowchart string
const mermaidChart = `
---
config:
  look: neo
  theme: default
---
flowchart LR
${connections.join('\n')}`;

// Output the result
return {
    json: {
        mermaidChart: mermaidChart
    }
};"
      },
      "typeVersion": 2
    },
    {
      "id": "28375139-c433-4c6c-a5ac-3d725c9b79ef",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2120,
        100
      ],
      "parameters": {
        "color": 3,
        "width": 470.91551628883894,
        "height": 419.34820384538847,
        "content": "## IMPORTANT NOTE FOR CLOUD USERS
### Since the cloud version doesn't support environmental variables, please update the following fields:

1. **instance_url**. Change the `{{$env[\"N8N_PROTOCOL\"]}}://{{$env[\"N8N_HOST\"]}}` expression to your cloud instance URL
2. **webhook_path**. Change the `{{$env[\"N8N_ENDPOINT_WEBHOOK\"] || \"webhook\"}}` simply to the `webhook`. So that the production webhook is called correclty."
      },
      "typeVersion": 1
    },
    {
      "id": "63245902-69d7-4d75-8cb3-58198208220a",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1220,
        360
      ],
      "webhookId": "dd9e2c5d-6c48-428e-aa54-bef9e369d3b0",
      "parameters": {
        "path": "dd9e2c5d-6c48-428e-aa54-bef9e369d3b0",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    }
  ],
  "active": true,
  "pinData": {},
  "settings": {
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveDataSuccessExecution": "all"
  },
  "versionId": "e73fe710-a873-4827-9a3f-2740b5479d62",
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "Respond with Mermaid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CONFIG": {
      "main": [
        [
          {
            "node": "Send Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "List workflows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Single workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "CONFIG",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List workflows": {
      "main": [
        [
          {
            "node": "Prepare workflow list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Single workflow": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare workflow list": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking ‘Test workflow’": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

功能特点

  • 自动检测新邮件
  • AI智能内容分析
  • 自定义分类规则
  • 批量处理能力
  • 详细的处理日志

技术分析

节点类型及作用

  • Manualtrigger
  • Respondtowebhook
  • N8N
  • Aggregate
  • Switch

复杂度评估

配置难度:
★★★★☆
维护难度:
★★☆☆☆
扩展性:
★★★★☆

实施指南

前置条件

  • 有效的Gmail账户
  • n8n平台访问权限
  • Google API凭证
  • AI分类服务订阅

配置步骤

  1. 在n8n中导入工作流JSON文件
  2. 配置Gmail节点的认证信息
  3. 设置AI分类器的API密钥
  4. 自定义分类规则和标签映射
  5. 测试工作流执行
  6. 配置定时触发器(可选)

关键参数

参数名称 默认值 说明
maxEmails 50 单次处理的最大邮件数量
confidenceThreshold 0.8 分类置信度阈值
autoLabel true 是否自动添加标签

最佳实践

优化建议

  • 定期更新AI分类模型以提高准确性
  • 根据邮件量调整处理批次大小
  • 设置合理的分类置信度阈值
  • 定期清理过期的分类规则

安全注意事项

  • 妥善保管API密钥和认证信息
  • 限制工作流的访问权限
  • 定期审查处理日志
  • 启用双因素认证保护Gmail账户

性能优化

  • 使用增量处理减少重复工作
  • 缓存频繁访问的数据
  • 并行处理多个邮件分类任务
  • 监控系统资源使用情况

故障排除

常见问题

邮件未被正确分类

检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。

Gmail认证失败

确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。

调试技巧

  • 启用详细日志记录查看每个步骤的执行情况
  • 使用测试邮件验证分类逻辑
  • 检查网络连接和API服务状态
  • 逐步执行工作流定位问题节点

错误处理

工作流包含以下错误处理机制:

  • 网络超时自动重试(最多3次)
  • API错误记录和告警
  • 处理失败邮件的隔离机制
  • 异常情况下的回滚操作